#ifndef SIMPLE_IO_READ_H
#define SIMPLE_IO_READ_H

#include <cstddef> // std::size_t std::byte
#include <type_traits> // std::enable_if_t
#include <variant> // std::variant std::visit
#include <string> // std::string
#include <utility> // std::forward

#include "simple/support/meta/pend.hpp" // support::meta::append_t

#include "handle.h" // operation interface
#include "error.h" // error
#include "standard.h" // standard_input_t
#include "read_write_common.h" // not_seekable socket_not_connected
#include "byte_range.hpp" // byte_range

//TODO: a lot of duplication between this and write.h/cpp ...

namespace simple::io
{
	namespace meta = support::meta;

	struct handle_is_directory : error {};
	struct system_out_of_resources_to_read : error {};
	struct bad_read_from_device : error {};
	struct socket_connection_reset : error {};
	struct socket_connection_timeout : error {};
	struct not_read_handle : error {};

	using common_read_result = std::variant<std::byte*,
		system_out_of_resources_to_read,
		bad_read_from_device,
		error
	>;

	using read_result = common_read_result;
	using read_offset_result = meta::append_t<common_read_result, not_seekable>;

	// TODO: these apply to sockets which should have their own interface type, distinct from files
	// the underlying unique_handle is the same though (close still works afaik) so on that level these are included
	using net_read_result = meta::append_t<read_result,
		socket_not_connected,
		socket_connection_reset,
		socket_connection_timeout
    >;

	using ex_read_result = meta::append_t<net_read_result,
		not_read_handle, handle_is_directory>;
	using ex_read_offset_result = meta::append_t<read_offset_result,
		not_read_handle, handle_is_directory>;

	[[nodiscard]]
	ex_read_result read(const unique_handle&, byte_range) noexcept;

	[[nodiscard]]
	ex_read_offset_result read(const unique_handle&, byte_range, std::size_t offset) noexcept;

	[[nodiscard]]
	ex_read_result read(standard_input_t, byte_range) noexcept;

	[[nodiscard]]
	ex_read_offset_result read(standard_input_t, byte_range, std::size_t offset) noexcept;

	template <operation allowed,
		std::enable_if_t<allowed && operation::read>* = nullptr>
	[[nodiscard]]
	read_result read(const interface<allowed>& i, byte_range bv) noexcept
	// TODO: return to<read_result>(read(i.handle, bv));
	{ return std::visit([](auto&& x){ return read_result(x); }, read(i.handle, bv)); }

	template <operation allowed,
		std::enable_if_t<allowed && operation::read>* = nullptr>
	[[nodiscard]]
	read_offset_result read(const interface<allowed>& i, byte_range bv, std::size_t off) noexcept
	// TODO: return to<read_offset_result>(read(i.handle, bv, off));
	{ return std::visit([](auto&& x){ return read_offset_result(x); }, read(i.handle, bv, off)); }

	// TODO: overload that would automatically apply as_byte_range to second argument, useful for printing arguments in case of errors

	constexpr struct read_f_t
	{
		template <typename... Args>
		[[nodiscard]] decltype(auto) operator()(Args&& ... args) const noexcept
		{ return simple::io::read(std::forward<Args>(args)...); }
	} read_f;

	[[nodiscard]] inline std::string get_message(const read_f_t&)
	{ return "simple::io::read"; }

	// TODO: move to a private header
	[[nodiscard]]
	ex_read_result read(handle, byte_range) noexcept;
	[[nodiscard]]
	ex_read_offset_result read(handle, byte_range, std::size_t offset) noexcept;


} // namespace simple::io

#endif /* end of include guard */
